Runtime防止Crash(unrecognized Selector sent to instance篇)

我相信对于所有的iOSer来说,最恐怖的就是线上的crash了。对于有(mei)强迫(you)症(qian)的我来说,只要一发现有crash,只要条件允许,无论是在凌晨三点还是在晚上十二点,我都会立刻拿出电脑来查找原因看看是哪里导致的crash。所以为了让程序不要crash,我们有两种方法,一种是预防,一种是补救。

今天我们的重点是预防,至于补救我在之前的一篇文章中有说过,大体就是通过上传crash堆栈信息,或者通过xcode拿到log,然后通过符号表还原定位到crash的代码。具体的可以看iOSCrash信息上报和处理

1. crash的原因

我们最常见的crash的原因不外乎以下几种:

  • unrecognized selector sent to instance
  • KVO crash
  • NSNotification crash
  • NSTimer crash
  • 数组越界,字典插入nil
  • 字符串处理crash
  • EXC_BAD_ACCESS

unrecognized selector sent to instance这个crash我相信所有的开发者都见过,这种问题一般在开发或者测试的时候可以发现并且很容易就修复了。

但是有一天,我们亲爱的服务端同事,本来是返回一个person的name属性,但是那个name是空的,在OC中,nil代表的是空,但是服务端返回的可能是一个“<NULL>”,OC解析出来会是一个NSNull的对象。如果我们在代码里面还有对name属性做一些字符串的处理,例如[name subString:],毫无疑问,这个APP会自杀给你看。

2. unrecognized selector的背后

拿上面的[name subStringFromIndex:1]来说,我们知道OC中的调用方法不是单纯的直接获取到方法函数的地址直接调用,而是一个消息发送的过程。先放一张图:

上面是消息转发的流程,也就是我今天重点要说的。

但是其实在这张图之前还有一个消息查找的流程,消息查找的流程如下:

  1. 如果是对象方法,首先会在对象的缓存方法里面查找,如果没找到则根据对象的isa指针,去类对象里面查找。
  2. 如果类对象里面没有查找到该方法,则会在该类对象的父类里面查找,查找遵循1,2的顺序
  3. 如果一直查找到根类都没有找到该方法的实现的话,就会开始消息的转发流程,也就是上图的流程
  4. 如果消息的转发依然没有找到对应的处理,则会crash。

3. 在哪里做拦截处理

如果我们要防止程序unrecognized selector crash,我们可以在消息的转发流程中做拦截处理,我们先来看看runtime给我们提供的三个消息处理的方法。

1. + (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
这个是消息转发的第一个方法,开发者可以在这个方法里面利用class_addMethod给该对象提供一个方法的实现,然后返回YES。如果返回了NO或者则会调用下面第二个方法。

2. - (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
当系统调用到该方法的时候,需要开发者返回一个id对象,如果返回了一个非nil的对象,那么系统会将该selector转发到该对象上去,也就是会重新开始消息的发送流程。如果返回了nil或者self,那么就会执行下面的第三步

`3. - (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE(“”);

  • (NSMethodSignature )methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(“”);`
    **
    系统会首先调用methodSignatureForSelector来获取一个方法的签名,如果返回了方法的签名则会创建一个NSInvocation对象并且调用forwardInvocation将消息转发给目标对象。***

接下来就有一个疑问了,我们应该在哪一步里面拦截消息会比较好。

  1. resolveInstanceMethod方法需要我们为该类添加一个实现,这是不必要的,因为本来那个类就不需要有那个方法,没必要为了防止crash而给它添加一个多余的实现。
  2. methodSignatureForSelector 以及 forwardInvocation 这里的开销比较大,需要创建invocation对象并且这里有可能会被开发者调用进行消息多重转发机制,所以这里也不太适合。
  3. 剩下的就只有forwardingTargetForSelector方法了,这个方法需要提供一个处理消息的类,我们正好可以创建一个用于保护的LMProtecter类,到时由该类里面处理就可以了。

4. 拦截的流程

我们拦截处理的流程大概有以下几点:

  1. 利用method_swizzle,创建一个NSObject的分类在load方法里面替换原来的forwardingTargetForSelector。
  2. 提供一个白名单机制,只对特定的类进行处理,例如常见的NSNull或或者是SDK中的类,通过类前缀来判断该类是否属于SDK的类。如果是系统的类(一般以_开头,则不进行拦截)
  3. 创建一个protect类,并且重写protect类的resolveInstanceMethod方法,提供一个方法的实现,当消息转发给protect类的时候,执行默认的实现,该方法会有一个返回值返回NSNull,这样当执行某些有返回值的方法的时候,可以避免其他的异常。为什么在该方法返回nul而不是nil或者其他对象呢,因为返回NUll正好在我们的白名单里面设置了null的判断,如果返回nil的话,可能会引发其他的例如将nil插入到字典中的错误。
  4. 在该方法里面进行异常的上报,下次版本修复该问题。

5. 关键代码:

原理和流程我们都说了,接下来代码实现就比较简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//
// NSObject+UnrecognizedSelectorProtecter.m
// UnRecognizedSelectorProtecter
//
// Created by lemon on 2019/4/12.
// Copyright © 2019年 Lemon. All rights reserved.
//

#import "NSObject+UnrecognizedSelectorProtecter.h"
#import <objc/runtime.h>
#import "LMProtecter.h"

static id protecter = nil;
static NSString *const ClassPrefix = @"LM";

@implementation NSObject (UnrecognizedSelectorProtecter)

+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
protecter = [[LMProtecter alloc]init];
swizzleMethod([self class], @selector(forwardingTargetForSelector:), @selector(swizzleForwardingTargetForSelector:));
});
}

#pragma mark - instance method
- (id)swizzleForwardingTargetForSelector:(SEL)aSelector{
//先执行原来的逻辑,如果已经外部提供一个对象,则不进行处理
id instance = [self swizzleForwardingTargetForSelector:aSelector];
if (instance) {
return instance;
}
//判断当前类是否在白名单中
BOOL isWhiteList = isWhiteListClass([self class]);
if (!isWhiteList) {
//如果是系统类则不做处理
return nil;
}
if (!instance) {
//将消息转发给procter对象
instance = protecter;
}
return instance;
}


#pragma mark - private method
static inline BOOL swizzleMethod(Class aClass,SEL originSelector , SEL swizzleSelector){
Method originMethod = class_getInstanceMethod(aClass, originSelector);
Method swizzleMethod = class_getInstanceMethod(aClass, swizzleSelector);

BOOL isAdded = class_addMethod(aClass, originSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));

if (isAdded) {
class_replaceMethod(aClass, swizzleSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else{
method_exchangeImplementations(originMethod, swizzleMethod);
}
return YES;
}

static inline BOOL isWhiteListClass(Class aClass){
NSString *classStr = NSStringFromClass(aClass);
//如果是下划线开头的代表是系统类,不对系统类进行处理
if ([classStr hasPrefix:@"_"]) {
return NO;
}
//如果是自己的类或者是NSNULL则进行处理
BOOL isNull = [classStr isEqualToString:NSStringFromClass([NSNull class])];
BOOL isBusinessCls = [classStr hasPrefix:classStr];
return isNull || isBusinessCls;
}

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//
// LMProtecter.m
// UnRecognizedSelectorProtecter
//
// Created by lemon on 2019/4/12.
// Copyright © 2019年 Lemon. All rights reserved.
//

#import "LMProtecter.h"
#import <objc/runtime.h>

@implementation LMProtecter

id protect_method_implementation(id self, SEL _cmd){
return [NSNull null];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
//消息会转发到这里来,动态的给Sel提供一个方法的实现就👌了
class_addMethod([self class], sel, (IMP)protect_method_implementation, "@@:");
NSLog(@"捕获到一个unRecognized Selector = %@ 崩溃信息",NSStringFromSelector(sel));
return YES;
}

//- (id)forwardingTargetForSelector:(SEL)aSelector{
// return [super forwardingTargetForSelector:aSelector];
//}
//
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
// return [super methodSignatureForSelector:aSelector];
//}
//
//- (void)forwardInvocation:(NSInvocation *)anInvocation{
// return [super forwardInvocation:anInvocation];
//}
//
//- (void)doesNotRecognizeSelector:(SEL)aSelector{
// return [super doesNotRecognizeSelector:aSelector];
//}

@end

全部代码已经放到git,需要的自取: LMProtecter

-------评论系统采用disqus,如果看不到需要翻墙-------------